domma-cms 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/CLAUDE.md +30 -0
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/mail-compose.js +1 -0
  5. package/admin/js/lib/wordpress-import-arrange.js +1 -0
  6. package/admin/js/templates/call-new.html +1 -1
  7. package/admin/js/templates/docs/usage-tools.html +13 -1
  8. package/admin/js/templates/wordpress-import.html +223 -0
  9. package/admin/js/views/call-new.js +1 -1
  10. package/admin/js/views/contacts.js +15 -15
  11. package/admin/js/views/form-submissions.js +2 -2
  12. package/admin/js/views/index.js +1 -1
  13. package/admin/js/views/wordpress-import.js +38 -0
  14. package/package.json +1 -1
  15. package/plugins/_lib/admin/mail/compose-window.js +3 -1
  16. package/plugins/_lib/admin/mail/identity.js +18 -3
  17. package/plugins/_lib/admin/mail/reader-view.js +32 -3
  18. package/plugins/blog/CLAUDE.md +4 -0
  19. package/plugins/blog/admin/lib/email.js +191 -0
  20. package/plugins/blog/admin/views/comments.js +3 -1
  21. package/plugins/blog/plugin.json +2 -2
  22. package/plugins/blog/plugin.public.js +1 -1
  23. package/plugins/blog/tests/email.test.js +69 -0
  24. package/plugins/booking/CLAUDE.md +4 -0
  25. package/plugins/booking/admin/lib/email.js +191 -0
  26. package/plugins/booking/admin/views/panels.js +7 -3
  27. package/plugins/booking/plugin.json +1 -1
  28. package/plugins/booking/tests/email.test.js +69 -0
  29. package/plugins/free-tier.lock.json +27 -19
  30. package/plugins/shopping-cart/CLAUDE.md +4 -0
  31. package/plugins/shopping-cart/admin/lib/email.js +191 -0
  32. package/plugins/shopping-cart/admin/views/orders.js +11 -3
  33. package/plugins/shopping-cart/plugin.json +2 -2
  34. package/plugins/shopping-cart/tests/email.test.js +69 -0
  35. package/plugins/support/CLAUDE.md +4 -0
  36. package/plugins/support/admin/lib/email.js +191 -0
  37. package/plugins/support/admin/views/desk.js +4 -1
  38. package/plugins/support/admin/views/inbox.js +6 -2
  39. package/plugins/support/plugin.json +2 -2
  40. package/plugins/support/tests/email.test.js +69 -0
  41. package/server/routes/api/wordpress-import.js +220 -0
  42. package/server/server.js +12 -0
  43. package/server/services/hooks.js +1 -1
  44. package/server/services/permissionRegistry.js +10 -0
  45. package/server/services/roles.js +1 -1
  46. package/server/services/tools.js +2 -1
  47. package/server/services/wpImport/convert.js +491 -0
  48. package/server/services/wpImport/fetch.js +264 -0
  49. package/server/services/wpImport/index.js +832 -0
  50. package/server/services/wpImport/plan.js +549 -0
  51. package/server/services/wpImport/store.js +242 -0
  52. package/server/services/wpImport/wxr.js +167 -0
  53. package/server/services/wpImport/xml.js +261 -0
  54. package/server/services/wpImport/zip.js +142 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.2] - 2026-10-15
4
+
5
+ - **Import from WordPress**, a free built-in Tool (Tools › Maintenance). Upload a WordPress export (Tools › Export in WordPress) and see a preview of every page, post, file and menu before anything changes. Pages keep their addresses; posts go to the Blog with categories, tags, featured images and approved comments; media comes from the old site or a zip of `wp-content/uploads`; old addresses, including `?p=123`, redirect to the new ones - and keep redirecting with the importer switched off; Yoast and Rank Math titles and descriptions carry over. Shortcodes and page-builder layouts with no equivalent are listed to rebuild. Run it again and it updates what it made, leaving pages you edited alone unless you choose Overwrite; an import stopped by a restart can be run again to finish it. Each run's report is kept (out of git). Downloads go only to the export's own site, never to private or the server's own addresses, and only on ports 80/443; SVG and anything that is HTML or script is never imported.
6
+
7
+ ## [1.7.1] - 2026-10-15
8
+
9
+ - **Email… opens Email Pro, not your desktop mail program.** In Contacts, form submissions and call links (and in Booking, Shopping Cart, Blog and Support), an Email action now opens Email Pro's compose window, already addressed, when you have a mailbox there; otherwise your device's mail program as before. Groups and selections can be emailed (everyone in Bcc). A prefilled message sits above your signature. A list too long for a mail link is copied for you to paste into Bcc, and says what was left out. New `admin/js/lib/mail-compose.js` (`openEmail`) for any screen - never `location.href = 'mailto:'`; the hand-off is `#/plugins/email-pro?compose=1&to=…`, read once by the mail reader.
10
+ - **Free Tools:** Booking 1.7.1, Shopping Cart 1.7.1, Blog 1.10.1 and Support 1.2.1 - their email actions go through `openEmail`, and stored addresses are encoded into links.
11
+
3
12
  ## [1.7.0] - 2026-10-15
4
13
 
5
14
  - **20 celebration scenes in every site.** Add New Year, Chinese New Year, Easter, Summer Garden, Winter, Autumn, Graduation, Birthday, Anniversary, standalone sparkler greetings, weather rain and Money Rain. Choose them in the Effects builder or right-click Celebrations menu, with controls for each scene's decorations.
package/CLAUDE.md CHANGED
@@ -320,6 +320,36 @@ the collection mirrors the form's `required` - and stamps `meta.outcome`. `notif
320
320
  forgetting triggers is the obvious failure. `goto-step` is announced as a `dm-form-trigger` DOM event
321
321
  rather than performed, since the wizard handle belongs to the embed.
322
322
 
323
+ ## Import from WordPress
324
+
325
+ A built-in Tool (`wordpress-import` in `services/tools.js`, permission `import.wordpress` - level 0, granted once to
326
+ `admin`), `#/wordpress-import`. Server: `server/services/wpImport/` + `routes/api/wordpress-import.js`
327
+ (`/api/import/wordpress`). Upload a WXR export -> preview (`planImport()`, pure, in `plan.js`) -> a background job
328
+ (`startRun()`, yields between items) -> a report kept in `content/imports/wordpress/` (history, reports, `map.json`).
329
+
330
+ - **The XML reader is its own** (`xml.js`): any DOCTYPE or ENTITY is refused, only the predefined and numeric
331
+ entities decode - do not swap in a parser that expands entities. Zips (`zip.js`) are read by offset with
332
+ zip-slip names dropped and ratio/size caps; nothing is ever extracted under its own name.
333
+ - **Media downloads go through `fetch.js` only**: the export's own hosts, every resolved address public, the
334
+ connection pinned to the checked address, never the server's own interface addresses, ports 80/443 only, no
335
+ redirect off-host; a non-image/PDF body that sniffs as HTML or script is refused (it would be served from /media).
336
+ `WP_IMPORT_ALLOW_PRIVATE_HOSTS=1` (ignored when `NODE_ENV=production`) is the tests' and local browser checks' way
337
+ to fetch from 127.0.0.1 on any port.
338
+ - **The mapping is saved as a run goes** (every few items and each phase), and `running.json` marks a run in
339
+ progress, so a restart mid-run is recorded as failed at boot and the re-run finishes it without duplicates.
340
+ `content/imports/wordpress/` is kept out of the site's git repo.
341
+ - **Re-runs**: `map.json` holds what each WordPress id became plus a fingerprint of what was written; a page,
342
+ post or menu whose fingerprint changed here is "Left as is" unless the person chooses Overwrite. A page moved in
343
+ the editor is followed (`content:pageRenamed` -> `followRename`).
344
+ - **Posts go to a Blog by feature, not by name** (`blogTarget()`): a switched-on plugin that ships `blog-posts`,
345
+ with the fields a post needs; comments only when `blog-comments` has the Blog's shape. Otherwise posts become
346
+ pages (under /blog, or /posts when a Blog is on).
347
+ - **Redirects**: path ones go into SEO's store (`seoEngine().addRule`); `/?p=`, `/?page_id=`, `/?attachment_id=`
348
+ are answered by the importer's own `onRequest` hook from `map.json`, because SEO's rules match paths only. That
349
+ hook is registered whatever the Tool switch says (the redirects belong to the migrated site) and re-reads
350
+ `map.json` when it changes.
351
+ - Unknown shortcodes are left as text with `[`/`]` brackets, so Domma never runs a WordPress `[button]`.
352
+
323
353
  ## Site search
324
354
 
325
355
  Search is part of the CMS, not a plugin - `server/services/search.js` (settings, `stripMarkdown`, `scorePage`,
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{views as K}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as J,sendToLogin as Y}from"./api.js";import{claimDrafts as Q}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as X}from"./lib/login-arrange.js";import{installHttpInterceptor as Z}from"./http-interceptor.js";import{installResizableSlideovers as tt}from"./lib/slideover-resizable.js";import{installActivityThrobber as et}from"./lib/activity-throbber.js";import{installUpdateNotice as it}from"./lib/update-notice.js";import{isPathAllowed as ot,redirectFor as at}from"./lib/admin-scope.js";import{deliverDesktop as st,desktopActive as nt,notificationSettings as lt}from"./lib/desktop-notify.js";import{avatarUrl as rt}from"./lib/avatar.js";import{installCalculator as ct}from"./lib/calculator.js";import{takeOverviewPass as mt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as pt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as dt}from"./lib/router-catch-up.js";$(()=>{Z(),ct(),it(),et(),tt(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,r="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||at(p,t);return i||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"&&!mt()?y[r]||r:null)}const y={},N={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Y();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),r=t.adminHome||"/";const i=t.permissions||[];return w=ot(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,r="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,r="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),lt.read().signInToasts)try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),pt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...K},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(G))l[t]&&(l[t]=O(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=_(l.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);l[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}l.dashboard=_(l.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(N[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=rt(t);$("#topbar-user").html(`
1
+ import{views as K}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as J,sendToLogin as Y}from"./api.js";import{claimDrafts as Q}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as X}from"./lib/login-arrange.js";import{installHttpInterceptor as Z}from"./http-interceptor.js";import{installResizableSlideovers as tt}from"./lib/slideover-resizable.js";import{installActivityThrobber as et}from"./lib/activity-throbber.js";import{installUpdateNotice as it}from"./lib/update-notice.js";import{isPathAllowed as ot,redirectFor as at}from"./lib/admin-scope.js";import{deliverDesktop as st,desktopActive as nt,notificationSettings as rt}from"./lib/desktop-notify.js";import{avatarUrl as lt}from"./lib/avatar.js";import{installCalculator as ct}from"./lib/calculator.js";import{takeOverviewPass as mt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as pt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as dt}from"./lib/router-catch-up.js";$(()=>{Z(),ct(),it(),et(),tt(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,l="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||at(p,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"&&!mt()?y[l]||l:null)}const y={},N={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Y();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),l=t.adminHome||"/";const i=t.permissions||[];return w=ot(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,l="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,l="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),rt.read().signInToasts)try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),pt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const r={...K},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(G))r[t]&&(r[t]=O(r[t],{plugin:t,licence:"Built in",...i}));r.dashboard=_(r.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);r[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}r.dashboard=_(r.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(N[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=lt(t);$("#topbar-user").html(`
2
2
  ${o?`<img id="topbar-avatar" class="topbar-avatar" src="${d(o)}" alt="">`:""}
3
3
  <span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
4
4
  <span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${e}</span>
@@ -20,4 +20,4 @@ import{views as K}from"./views/index.js";import{withPluginChrome as O}from"./lib
20
20
  <span data-icon="log-out"></span>
21
21
  <span>Sign out</span>
22
22
  </a>
23
- `);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),J()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),F(),u||(u=setInterval(F,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function F(){if(!(!m()||g===!1))try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&nt()&&st(await c.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=X(window.location.hash));else if(Q(x()?.id),await U(),x()){const a=await f(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(a),B()}const t=R._extractParams.bind(R);R._extractParams=function(o,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),o.endsWith("/*")){const s=o.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(o,a)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(o){return m()&&e!==o&&(e=o,await U(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},dt(R),R.init({container:"#view-container",routes:I,views:l,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
23
+ `);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),J()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),F(),u||(u=setInterval(F,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function F(){if(!(!m()||g===!1))try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&nt()&&st(await c.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=X(window.location.hash));else if(Q(x()?.id),await U(),x()){const a=await f(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(a),B()}const t=R._extractParams.bind(R);R._extractParams=function(o,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),o.endsWith("/*")){const s=o.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(o,a)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(o){return m()&&e!==o&&(e=o,await U(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},dt(R),R.init({container:"#view-container",routes:I,views:r,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
@@ -0,0 +1 @@
1
+ import{isPathAllowed as m}from"./admin-scope.js";export const MAIL_PLUGIN="email-pro",MAIL_ROUTE=`#/plugins/${MAIL_PLUGIN}`;const d=2e3;export const MAILTO_LIMIT=1800;export function looksLikeAddress(t){return typeof t=="string"&&/^[^\s@<>,;"]+@[^\s@<>,;"]+$/.test(t)}export function recipientList(t){const e=new Set,n=[],o=(Array.isArray(t)?t:t==null?[]:[t]).flatMap(c=>typeof c=="string"?c.split(/[,;]/).map(f):[c]);for(const c of o){const s=String(typeof c=="string"?c:c?.address??c?.email??"").trim();if(!looksLikeAddress(s))continue;const r=s.toLowerCase();if(e.has(r))continue;e.add(r);const a=typeof c=="object"&&c?String(c.name??"").replace(/[\r\n<>",;]+/g," ").replace(/\s+/g," ").trim():"";n.push({name:a,address:s})}return n}function f(t){const e=String(t).trim().match(/^(.*?)\s*<([^<>]+)>$/);return e?{name:e[1].replace(/^["']|["']$/g,""),address:e[2].trim()}:String(t).trim()}function h(t){return t.map(e=>e.name?`${e.name} <${e.address}>`:e.address).join(", ")}function l(t){return recipientList(String(t??""))}function u(t,{oneLine:e=!1}={}){let n=String(t??"");return e&&(n=n.replace(/[\r\n]+/g," ")),n.slice(0,d).trim()}export function composeRequest({to:t,cc:e,bcc:n,subject:o,body:c}={}){return{to:recipientList(t),cc:recipientList(e),bcc:recipientList(n),subject:u(o,{oneLine:!0}),body:u(c)}}export function composeHash(t){const e=composeRequest(t),n=new URLSearchParams({compose:"1"});for(const o of["to","cc","bcc"])e[o].length&&n.set(o,h(e[o]));return e.subject&&n.set("subject",e.subject),e.body&&n.set("body",e.body),`${MAIL_ROUTE}?${n}`}export function readComposeRequest(t){const e=String(t??"").match(/^#\/plugins\/[\w-]+\?(.*)$/);if(!e)return null;const n=new URLSearchParams(e[1]);return n.get("compose")!=="1"?null:composeRequest({to:l(n.get("to")),cc:l(n.get("cc")),bcc:l(n.get("bcc")),subject:n.get("subject")??"",body:n.get("body")??""})}export function mailtoHref(t){const e=composeRequest(t),n=c=>c.map(s=>encodeURIComponent(s.address).replace(/%40/g,"@")).join(","),o=[];return e.cc.length&&o.push(`cc=${n(e.cc)}`),e.bcc.length&&o.push(`bcc=${n(e.bcc)}`),e.subject&&o.push(`subject=${encodeURIComponent(e.subject)}`),e.body&&o.push(`body=${encodeURIComponent(e.body)}`),`mailto:${n(e.to)}${o.length?`?${o.join("&")}`:""}`}export function composeAvailable({routes:t,accounts:e,scope:n=null}={}){const o=`/plugins/${MAIL_PLUGIN}`;return Array.isArray(t)&&t.some(s=>s?.path===o)&&m(n,o)&&Array.isArray(e)&&e.length>0}export function allAddresses(t){const e=composeRequest(t);return[...e.to,...e.cc,...e.bcc].map(n=>n.address)}export function copyNotice(t){const e=composeRequest(t),n=allAddresses(e).length,o=e.bcc.length?"Bcc":"To",c=n===1?"the address":`${n} addresses`,s=[e.subject&&`the subject "${e.subject.length>60?`${e.subject.slice(0,57)}...`:e.subject}"`,e.body&&"the message"].filter(Boolean),r=s.length?` ${s.join(" and ").replace(/^./,a=>a.toUpperCase())} ${s.length>1?"weren't":"wasn't"} included.`:"";return`Copied ${c} - paste ${n===1?"it":"them"} into ${o}.${r}`}const g=1e4;let i=null;export function emailReady(){if(i&&Date.now()-i.at<g)return i.answer;const t={at:Date.now(),answer:null};return t.answer=(async()=>{const e=await b();return!e&&i===t&&(i=null),e})(),i=t,t.answer}async function b(){try{const{apiRequest:t}=await import("/admin/js/api.js"),[e,n]=await Promise.all([t("/plugins/admin-config",{method:"GET"}),t("/auth/permissions",{method:"GET"}).catch(()=>null)]),o=n?.adminScope||null;if(!composeAvailable({routes:e?.routes,accounts:[1],scope:o}))return!1;const c=await t(`/plugins/${MAIL_PLUGIN}/accounts`,{method:"GET"});return composeAvailable({routes:e.routes,accounts:c?.accounts,scope:o})}catch{return!1}}function p(t,e="info"){try{globalThis.E?.toast?.(t,{type:e})}catch{}}async function y(t){try{if(!navigator.clipboard?.writeText)throw new Error("no async clipboard");return await navigator.clipboard.writeText(t),!0}catch{try{const e=document.createElement("textarea");e.value=t,e.style.cssText="position:fixed;opacity:0",document.body.appendChild(e),e.select();const n=document.execCommand("copy");return e.remove(),n}catch{return!1}}}function w(t){const e=allAddresses(t),n=E.modal({title:`${e.length} ${e.length===1?"address":"addresses"}`,size:"md"}),o=document.createElement("div");o.className="flex flex-col gap-2";const c=document.createElement("p");c.className="text-sm",c.textContent=`Too many for your mail program to take from a link, and the browser would not copy them. Select them, copy, and paste into ${t.bcc.length?"Bcc":"To"}.`;const s=document.createElement("textarea");if(s.className="form-textarea mail-compose-addresses",s.readOnly=!0,s.rows=8,s.value=e.join(", "),o.append(c,s),t.subject){const r=document.createElement("label");r.className="form-label",r.textContent="Subject";const a=document.createElement("input");a.className="form-input",a.readOnly=!0,a.value=t.subject,o.append(r,a)}n.element.appendChild(o),n.open(),setTimeout(()=>{s.focus(),s.select()},60)}export async function openEmail(t){const e=composeRequest(t);if(!e.to.length&&!e.cc.length&&!e.bcc.length)return p("There is no valid email address to write to.","warning"),"none";if(await emailReady())return location.hash=composeHash(e),"compose";const n=mailtoHref(e);return n.length>MAILTO_LIMIT?await y(allAddresses(e).join(", "))?(p(copyNotice(e),"info"),"copied"):(w(e),"shown"):(location.href=n,"mailto")}
@@ -0,0 +1 @@
1
+ export const KIND_LABEL={page:"Page",post:"Post",media:"Media",menu:"Menu",category:"Category"},KIND_PLURAL={page:"Pages",post:"Posts",media:"Media",menu:"Menus",category:"Categories"},KIND_ICON={page:"file-text",post:"edit-3",media:"image",menu:"menu",category:"tag"},ACTION={create:{text:"New",tone:"ok"},rename:{text:"New, renamed",tone:"warn"},update:{text:"Update",tone:"info"},keep:{text:"Left as is",tone:"muted"},skip:{text:"Skipped",tone:"muted"}},RESULT={created:{text:"Made",tone:"ok"},updated:{text:"Updated",tone:"info"},kept:{text:"Left as is",tone:"muted"},skipped:{text:"Skipped",tone:"muted"},failed:{text:"Failed",tone:"bad"}},PLAN_FILTERS=[{id:"all",label:"All"},{id:"new",label:"New"},{id:"update",label:"Updates"},{id:"renamed",label:"Renamed"},{id:"keep",label:"Left as is"},{id:"skip",label:"Skipped"},{id:"rebuild",label:"Needs rebuilding"}],REPORT_FILTERS=[{id:"all",label:"All"},{id:"created",label:"Made"},{id:"updated",label:"Updated"},{id:"kept",label:"Left as is"},{id:"skipped",label:"Skipped"},{id:"failed",label:"Failed"},{id:"rebuild",label:"Needs rebuilding"}];const d=e=>!!(e.builder||e.shortcodes?.length);export function planMatches(e,t){switch(t){case"new":return e.action==="create"||e.action==="rename";case"update":return e.action==="update";case"renamed":return e.action==="rename"||!!e.replaces;case"keep":return e.action==="keep";case"skip":return e.action==="skip";case"rebuild":return d(e);default:return!0}}export function reportMatches(e,t){return t==="rebuild"?d(e):t==="all"||!t||e.result===t}export function counts(e,t,i,o=""){const n=e.filter(s=>!o||s.kind===o);return Object.fromEntries(t.map(s=>[s.id,n.filter(a=>i(a,s.id)).length]))}export function kindTabs(e){const t=["page","post","media","menu","category"],i={};for(const o of e)i[o.kind]=(i[o.kind]||0)+1;return[{id:"",label:"Everything",count:e.length},...t.filter(o=>i[o]).map(o=>({id:o,label:KIND_PLURAL[o],count:i[o]}))]}export function planRow(e){const t=ACTION[e.action]||ACTION.skip,i=e.target?e.kind==="menu"?`Menu "${e.target}"`:e.target:"",o=[KIND_LABEL[e.kind]||e.kind];return e.status&&(e.kind==="page"||e.kind==="post")&&o.push(e.status),e.comments&&o.push(`${e.comments} comment${e.comments===1?"":"s"}`),e.count&&o.push(`${e.count} item${e.count===1?"":"s"}`),{key:e.key,kind:e.kind,icon:KIND_ICON[e.kind]||"file",title:e.title||"(untitled)",pill:e.replaces?"Replaces a page":t.text,pillClass:`is-${e.replaces?"warn":t.tone}`,where:i,meta:o.join(" \xB7 "),reason:e.reason||"",rebuild:[e.builder?`Built with ${e.builder}`:"",...(e.shortcodes||[]).map(n=>`[${n}]`)].filter(Boolean).join(" "),chosen:!!e.choice}}export function reportRow(e){const t=RESULT[e.result]||RESULT.skipped;return{key:e.key,kind:e.kind,icon:KIND_ICON[e.kind]||"file",title:e.title||"(untitled)",pill:t.text,pillClass:`is-${t.tone}`,where:e.target||"",meta:[KIND_LABEL[e.kind]||e.kind,e.format==="html"?"kept as HTML":""].filter(Boolean).join(" \xB7 "),reason:[e.reason,...e.notes||[]].filter(Boolean).join(" "),rebuild:[e.builder?`Built with ${e.builder}`:"",...(e.shortcodes||[]).map(i=>`[${i}]`)].filter(Boolean).join(" ")}}export function choiceItems(e,t){const i=[];return t&&i.push({id:"reset",label:"Undo my choice",icon:"rotate-ccw"}),t!=="skip"&&e.action!=="skip"&&e.action!=="keep"&&i.push({id:"skip",label:"Skip this",icon:"x-circle"}),t!=="overwrite"&&e.action==="keep"&&i.push({id:"overwrite",label:"Overwrite my changes here",icon:"refresh-cw"}),t!=="overwrite"&&e.action==="rename"&&(e.kind==="page"||e.kind==="post")&&e.wanted&&e.kind==="page"&&i.push({id:"overwrite",label:`Replace the page at ${e.wanted}`,icon:"refresh-cw"}),t!=="import"&&e.action==="skip"&&/Deleted (on this site|from the Blog)/.test(e.reason||"")&&i.push({id:"import",label:"Import again",icon:"download"}),i}export function applyChoice(e,t,i){const o={...e||{}};return!i||i==="reset"?delete o[t]:o[t]=i,o}export function defaultOptions(e=!1){return{posts:e?"blog":"pages",media:"download",comments:!0,menus:!0,redirects:!0,drafts:!0,choices:{}}}export function cleanOptions(e={},t=!1){const i=defaultOptions(t),o=(n,s,a)=>s.includes(n)?n:a;return{posts:o(e.posts,t?["blog","pages","skip"]:["pages","skip"],i.posts),media:o(e.media,["download","zip","skip"],i.media),comments:e.comments!==!1,menus:e.menus!==!1,redirects:e.redirects!==!1,drafts:e.drafts!==!1,choices:Object.fromEntries(Object.entries(e.choices||{}).filter(([n,s])=>typeof n=="string"&&["skip","overwrite","import"].includes(s)))}}export function optionsSnapshot(e){const t=cleanOptions(e,!0),i=Object.keys(t.choices).sort().map(o=>`${o}=${t.choices[o]}`).join(",");return[t.posts,t.media,t.comments,t.menus,t.redirects,t.drafts,i].join("|")}export const isDirty=(e,t)=>optionsSnapshot(e)!==optionsSnapshot(defaultOptions(t)),draftKey=e=>e?`wordpress-import.draft.${e}`:"";export function liveDrafts(e,t,i){if(!e||typeof e!="object"||!e.items||typeof e.items!="object")return{items:{},pruned:!1};if(e.owner&&i&&e.owner!==i)return{items:{},pruned:!1};const o={};let n=!1;for(const[s,a]of Object.entries(e.items))t.includes(s)&&a?.options&&a.was===s?o[s]=a:n=!0;return{items:o,pruned:n}}export function withDraft(e,t,i,o){const n={...e&&e.owner===t&&e.items||{}};return o?n[i]=o:delete n[i],{owner:t,items:n}}export function draftQuestion({when:e="",file:t=""}={}){return{title:"Choices you left last time",text:`You changed the import options${t?` for ${t}`:""}${e?` (${e})`:""} and left before running it. Throw away those choices and start from the defaults?`,confirmText:"Throw away",cancelText:"Keep them"}}export function progressText(e){if(!e)return"";const t=e.total?` \xB7 ${e.done} of ${e.total}`:"";return`${e.phase||"Working"}${t}${e.current?` \xB7 ${String(e.current).slice(0,80)}`:""}`}export const progressPercent=e=>e?.total?Math.min(100,Math.round(e.done/e.total*100)):0;export function planTiles(e){const t=e?.summary?.counts||{},i=o=>t[o]?t[o].total-t[o].skip:0;return[{id:"page",label:"Pages",value:i("page"),of:t.page?.total||0},{id:"post",label:"Posts",value:i("post"),of:t.post?.total||0},{id:"media",label:"Media",value:i("media"),of:t.media?.total||0},{id:"menu",label:"Menus",value:i("menu"),of:t.menu?.total||0},{id:"comments",label:"Comments",value:e?.summary?.comments||0,of:0},{id:"redirects",label:"Redirects",value:e?.summary?.redirects||0,of:0}].map(o=>({...o,sub:o.of&&o.of!==o.value?`of ${o.of}`:""}))}export function historyRow(e,t=i=>i){const i=e.summary||{},o=Object.values(i.kinds||{}).reduce((a,l)=>a+(l.created||0),0),n=Object.values(i.kinds||{}).reduce((a,l)=>a+(l.updated||0),0),s=[];return o&&s.push(`${o} made`),n&&s.push(`${n} updated`),i.redirects?.created&&s.push(`${i.redirects.created} redirect${i.redirects.created===1?"":"s"}`),i.rebuild&&s.push(`${i.rebuild} to rebuild`),i.failed&&s.push(`${i.failed} failed`),e.status==="failed"&&s.splice(0,s.length,"Stopped before the end - run it again to finish"),{id:e.id,title:e.siteTitle||e.source||"WordPress site",meta:[t(e.at),e.by,e.file].filter(Boolean).join(" \xB7 "),summary:s.join(", ")||"Nothing changed",pill:e.status==="failed"?"Stopped":e.status==="partial"?"Some failed":"Done",pillClass:e.status==="failed"||e.status==="partial"?"is-bad":"is-ok"}}export function stagedRow(e,t=i=>i){const i=e.counts||{},o=[i.page&&`${i.page} pages`,i.post&&`${i.post} posts`,i.attachment&&`${i.attachment} media`].filter(Boolean);return{id:e.id,title:e.siteTitle||e.fileName||"WordPress export",meta:[e.fileName,t(e.at),o.join(", ")].filter(Boolean).join(" \xB7 "),pill:e.uploads?"Uploads zip added":"Waiting",pillClass:e.uploads?"is-ok":"is-muted"}}
@@ -51,7 +51,7 @@
51
51
  <input id="cnx-link" class="form-input cnx-link" readonly data-bind-value="joinUrl.value" data-on-focus="selectLink">
52
52
  <div class="cnx-actions is-start">
53
53
  <button type="button" class="btn btn-sm btn-outline" data-on-click="copyLink"><span data-icon="copy" data-icon-size="13"></span> Copy</button>
54
- <a class="btn btn-sm btn-ghost" data-bind-href="mailto.value"><span data-icon="mail" data-icon-size="13"></span> Email it…</a>
54
+ <a class="btn btn-sm btn-ghost" data-bind-href="mailto.value" data-on-click="emailIt"><span data-icon="mail" data-icon-size="13"></span> Email it…</a>
55
55
  <span class="cnx-spacer"></span>
56
56
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="close">Done</button>
57
57
  </div>
@@ -7,7 +7,7 @@
7
7
  <div class="col-12">
8
8
  <div class="docs-body">
9
9
 
10
- <p>Contacts, Notes, Todo, Calculator, Analytics and SEO are part of the CMS. In the sidebar's Tools folder
10
+ <p>Contacts, Notes, Todo, Calculator, Analytics, SEO and Import from WordPress are part of the CMS. In the sidebar's Tools folder
11
11
  they start in groups by what they are for, alongside the Marketplace Tools:
12
12
  <strong>Productivity</strong> (Notes, Todo, Calculator, Calendar), <strong>Communication</strong>
13
13
  (Contacts, email, Messages), <strong>Marketing</strong> (Analytics, SEO, Feedback),
@@ -140,6 +140,18 @@
140
140
  IndexNow, page speed, rich results, sitemap rules, a not-found log, a link monitor and SEO
141
141
  history. Its own guide is under <a href="#/docs/plugins">Plugin guides</a>.</p>
142
142
 
143
+ <h3>Import from WordPress</h3>
144
+ <p>Brings a WordPress site across: <strong>Tools &gt; Maintenance &gt; Import from WordPress</strong>.
145
+ In WordPress, export everything (<strong>Tools &gt; Export &gt; All content</strong>) and upload the
146
+ .xml file. The preview shows what each page, post, file and menu will become before anything
147
+ changes: pages keep their WordPress addresses where they are free (a taken one gets the next free
148
+ address), posts go to the Blog (or become pages), media comes from the old site while it is still
149
+ online or from a zip of its <code>wp-content/uploads</code> folder, and the old addresses -
150
+ including <code>?p=123</code> links - redirect to the new ones. Yoast and Rank Math titles and
151
+ descriptions carry over. Shortcodes and page-builder layouts this site has no equivalent for are
152
+ listed as <em>needs rebuilding</em>. Run it again later and it updates what it made, leaving alone
153
+ anything you have edited here since unless you choose Overwrite. Each run's report is kept.</p>
154
+
143
155
  <h3>Notifications</h3>
144
156
  <p>The bell in the admin, and <strong>System &gt; Notifications</strong>. Opening the screen marks
145
157
  everything read. Its settings choose which <strong>sources</strong> notify which roles, and
@@ -0,0 +1,223 @@
1
+ <!-- Tools › Import from WordPress. Bound with M.applyBindings - see admin/js/views/wordpress-import.js, which also
2
+ inserts the row templates (the router renders a templateUrl through _.render, eating any {{…}}).
3
+ No page heading: the Tool banner above is the heading. -->
4
+ <div class="wpx">
5
+ <div class="wpx-note dm-loading-slot wpx-loading">Loading…</div>
6
+ <p class="wpx-alert is-bad wpx-failed" role="alert" hidden></p>
7
+ <div class="wpx-body" hidden>
8
+
9
+ <!-- A run in progress -->
10
+ <div class="wpx-card wpx-running" hidden data-bind-hidden="!job.value">
11
+ <div class="wpx-card-title"><span data-icon="download" data-icon-size="16"></span>
12
+ <span data-bind-text="jobTitle.value"></span></div>
13
+ <div class="wpx-bar" role="progressbar" aria-label="Import progress"><span class="wpx-bar-fill"></span></div>
14
+ <p class="wpx-note" data-bind-text="jobText.value"></p>
15
+ <p class="wpx-note">You can leave this screen - the import carries on, and its report is kept under Past imports.</p>
16
+ </div>
17
+
18
+ <!-- Start: upload, and uploads waiting -->
19
+ <div class="wpx-start" hidden data-bind-hidden="mode.value !== 'start'">
20
+ <div class="wpx-card">
21
+ <div class="wpx-card-title">Upload a WordPress export
22
+ <span data-help-title="Where the export comes from" data-help="In WordPress, go to Tools › Export, choose All content and press Download Export File. That gives you an .xml file (some hosts zip it - a .zip holding it works too). It holds your pages, posts, comments, menus and the addresses of your media - not the media files themselves; those are fetched from your old site, or from a zip of its wp-content/uploads folder, on the next screen."></span></div>
23
+ <p class="wpx-note">Nothing on this site changes until you have seen the preview and pressed <strong>Run import</strong>. Up to <span data-bind-text="wxrLimit.value"></span>.</p>
24
+ <label class="wpx-drop" data-bind-class="dropping.value && 'is-over'">
25
+ <input type="file" class="wpx-file" accept=".xml,.zip,application/xml,text/xml,application/zip">
26
+ <span data-icon="upload" data-icon-size="22"></span>
27
+ <span><strong>Choose the export file</strong> or drop it here</span>
28
+ </label>
29
+ <div class="wpx-upload" hidden data-bind-hidden="!uploadText.value">
30
+ <div class="wpx-bar"><span class="wpx-bar-fill wpx-upload-fill"></span></div>
31
+ <p class="wpx-note" data-bind-text="uploadText.value"></p>
32
+ </div>
33
+ <p class="wpx-note is-bad" hidden data-bind-hidden="!uploadError.value" data-bind-text="uploadError.value"></p>
34
+ </div>
35
+
36
+ <div class="wpx-card" hidden data-bind-hidden="!stagedRows.value.length">
37
+ <div class="wpx-card-title">Waiting to run
38
+ <span data-help="Exports you uploaded but have not imported yet. Click one to see its preview. Right-click (or ⋮, or Shift+F10) to discard it. An upload not run within a day is removed."></span></div>
39
+ <div class="wpx-list wpx-staged" data-each="stagedRows.value key=id"></div>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Preview -->
44
+ <div class="wpx-preview" hidden data-bind-hidden="mode.value !== 'preview'">
45
+ <div class="wpx-card wpx-head">
46
+ <div class="wpx-head-main">
47
+ <strong class="wpx-site" data-bind-text="siteTitle.value"></strong>
48
+ <small class="wpx-note" data-bind-text="siteMeta.value"></small>
49
+ </div>
50
+ <div class="wpx-head-actions">
51
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="back"><span data-icon="arrow-left" data-icon-size="14"></span> Back</button>
52
+ <button type="button" class="btn btn-sm btn-outline" data-on-click="discard">Discard</button>
53
+ <button type="button" class="btn btn-sm btn-primary wpx-run" data-on-click="run" data-bind-disabled="!canRun.value"><span data-icon="play" data-icon-size="14"></span> Run import</button>
54
+ </div>
55
+ </div>
56
+ <p class="wpx-alert is-dirty" hidden data-bind-hidden="!dirty.value">
57
+ <span data-icon="edit-3" data-icon-size="16"></span>
58
+ <span>Your choices are kept for this upload if you leave before running it.</span>
59
+ <span class="wpx-spacer"></span>
60
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="resetChoices">Back to the defaults</button></p>
61
+
62
+ <div class="wpx-card">
63
+ <div class="wpx-card-title">What to import</div>
64
+ <div class="wpx-options">
65
+ <div class="wpx-option">
66
+ <span class="form-label">Posts
67
+ <span data-help-title="Posts" data-help="To the Blog: each post becomes a Blog post, with its categories, tags, featured image, author (matched by email to a user here, otherwise you) and approved comments. As pages: each post becomes a page - under /blog when the site has no Blog switched on, under /posts when it has (the Blog answers at its own address). A post scheduled in WordPress stays scheduled in the Blog; drafts, pending and private posts arrive as drafts."></span></span>
68
+ <div class="wpx-seg" role="group" aria-label="Posts">
69
+ <button type="button" data-on-click="postsBlog" data-bind-class="opt.value.posts === 'blog' && 'is-on'" data-bind-disabled="!blogAvailable.value">To the Blog</button>
70
+ <button type="button" data-on-click="postsPages" data-bind-class="opt.value.posts === 'pages' && 'is-on'">As pages</button>
71
+ <button type="button" data-on-click="postsSkip" data-bind-class="opt.value.posts === 'skip' && 'is-on'">Leave out</button>
72
+ </div>
73
+ <small class="wpx-note" hidden data-bind-hidden="!blogNote.value" data-bind-text="blogNote.value"></small>
74
+ </div>
75
+ <div class="wpx-option">
76
+ <span class="form-label">Media files
77
+ <span data-help-title="Media files" data-help="From the old site: each file is downloaded from the WordPress site's own address while it is still online (only that site - never anywhere else). From a zip: zip the wp-content/uploads folder (your host's file manager or FTP) and upload it below - use this when the old site is offline. Leave out: pages keep pointing at the old site's files. SVG files are never imported (they can carry scripts)."></span></span>
78
+ <div class="wpx-seg" role="group" aria-label="Media files">
79
+ <button type="button" data-on-click="mediaDownload" data-bind-class="opt.value.media === 'download' && 'is-on'">From the old site</button>
80
+ <button type="button" data-on-click="mediaZip" data-bind-class="opt.value.media === 'zip' && 'is-on'">From a zip</button>
81
+ <button type="button" data-on-click="mediaSkip" data-bind-class="opt.value.media === 'skip' && 'is-on'">Leave out</button>
82
+ </div>
83
+ <div class="wpx-zip" hidden data-bind-hidden="opt.value.media !== 'zip'">
84
+ <small class="wpx-note" data-bind-text="zipText.value"></small>
85
+ <div class="wpx-zip-actions">
86
+ <label class="btn btn-sm btn-outline wpx-zip-pick"><input type="file" class="wpx-zip-file" accept=".zip,application/zip" hidden>
87
+ <span data-bind-text="zipButton.value"></span></label>
88
+ <button type="button" class="btn btn-sm btn-ghost" hidden data-bind-hidden="!zipInfo.value" data-on-click="removeZip">Remove</button>
89
+ </div>
90
+ <div class="wpx-upload" hidden data-bind-hidden="!zipUploadText.value">
91
+ <div class="wpx-bar"><span class="wpx-bar-fill wpx-zip-fill"></span></div>
92
+ <small class="wpx-note" data-bind-text="zipUploadText.value"></small>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ <div class="wpx-toggles">
97
+ <label class="wpx-toggle"><input type="checkbox" class="form-switch" data-model="optComments.value" data-bind-disabled="opt.value.posts !== 'blog'"> Approved comments
98
+ <span data-help="Approved comments go to the Blog's comments, replies under the comment they answer. Spam, unapproved comments, pingbacks and trackbacks are never imported."></span></label>
99
+ <label class="wpx-toggle"><input type="checkbox" class="form-switch" data-model="optMenus.value"> Menus
100
+ <span data-help="Each WordPress menu becomes a menu here (wp-…), pointing at the new addresses. It is not put on the site until you choose it for the navbar or a footer in Menus › Locations."></span></label>
101
+ <label class="wpx-toggle"><input type="checkbox" class="form-switch" data-model="optRedirects.value"> Redirects from the old addresses
102
+ <span data-help-title="Redirects" data-help="Links and search results that point at the old addresses keep working: each old page, post, category and tag address answers with a permanent (301) redirect to its new one, in SEO › Redirects. ?p=123 and ?page_id=45 addresses are answered too. An address a page on this site already uses is never redirected."></span></label>
103
+ <label class="wpx-toggle"><input type="checkbox" class="form-switch" data-model="optDrafts.value"> Drafts and private items
104
+ <span data-help="Drafts, pending, private and password-protected pages and posts - imported as drafts, so nobody sees them until you publish them. Items in the WordPress bin are never imported."></span></label>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="wpx-note dm-loading-slot wpx-plan-loading" data-bind-hidden="!planLoading.value">Reading the export…</div>
110
+ <p class="wpx-note is-bad" hidden data-bind-hidden="!planError.value" data-bind-text="planError.value"></p>
111
+
112
+ <div class="wpx-plan" hidden data-bind-hidden="!plan.value">
113
+ <div class="wpx-tiles" data-each="tiles.value key=id"></div>
114
+ <div class="wpx-notes" hidden data-bind-hidden="!notes.value.length">
115
+ <div class="wpx-note-list" data-each="notes.value key=text"></div>
116
+ </div>
117
+
118
+ <div class="wpx-card">
119
+ <div class="wpx-card-title">Item by item
120
+ <span data-help-title="Item by item" data-help="What happens to each page, post, file and menu. New: made here. Renamed: its address is taken here, so it gets the next free one (the old address redirects to it). Update: made by an earlier import and unchanged here since - refreshed. Left as is: made by an earlier import but edited here since - not touched unless you choose Overwrite. Right-click (or ⋮, or Shift+F10) an item to skip it or change what happens. Needs rebuilding: shortcodes and page builders this site has no equivalent for - the text stays, the layout is yours to rebuild."></span>
121
+ <span class="wpx-spacer"></span>
122
+ <small class="wpx-note" hidden data-bind-hidden="!refreshing.value">Updating…</small></div>
123
+ <div class="wpx-bar-row">
124
+ <div class="wpx-seg wpx-kinds" role="group" aria-label="Kind" data-each="kinds.value key=id"></div>
125
+ </div>
126
+ <div class="wpx-bar-row">
127
+ <div class="wpx-seg wpx-filters" role="group" aria-label="Show" data-each="filters.value key=id"></div>
128
+ <input type="search" class="form-input wpx-search" placeholder="Search titles and addresses…" aria-label="Search items" data-model="query.value">
129
+ </div>
130
+ <p class="wpx-note" hidden data-bind-hidden="rows.value.length" data-bind-text="emptyText.value"></p>
131
+ <div class="wpx-list wpx-items" data-each="rows.value key=key"></div>
132
+ <p class="wpx-note" hidden data-bind-hidden="!moreText.value" data-bind-text="moreText.value"></p>
133
+ </div>
134
+
135
+ <div class="wpx-card" hidden data-bind-hidden="!redirectRows.value.length">
136
+ <div class="wpx-card-title">Redirects
137
+ <span data-bind-text="redirectCount.value" class="wpx-chip"></span>
138
+ <span data-help="The old addresses that will send visitors (and search engines) on to the new ones. Skipped ones say why."></span></div>
139
+ <div class="wpx-list wpx-redirects" data-each="redirectRows.value key=from"></div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- History -->
145
+ <div class="wpx-card" hidden data-bind-hidden="mode.value === 'preview'">
146
+ <div class="wpx-card-title">Past imports
147
+ <span data-help="Every import run on this site, with its report: what was made, the redirects added, the shortcodes and layouts to rebuild, and anything that failed. Click one to read its report. Removing one from this list keeps everything it imported."></span></div>
148
+ <p class="wpx-note" hidden data-bind-hidden="historyRows.value.length">Nothing imported yet.</p>
149
+ <div class="wpx-list wpx-history" data-each="historyRows.value key=id"></div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <style>
155
+ .wpx, .wpx-body, .wpx-start, .wpx-preview, .wpx-plan { display: flex; flex-direction: column; gap: .75rem; }
156
+ .wpx [hidden] { display: none !important; }
157
+ .wpx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
158
+ .wpx-note.is-bad { color: var(--dm-danger); }
159
+ .wpx-alert { margin: 0; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; padding: .6rem .8rem; border-radius: var(--dm-radius-md);
160
+ background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
161
+ .wpx-alert.is-bad { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-danger); }
162
+ .wpx-alert.is-dirty { background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
163
+ .wpx-spacer { flex: 1; }
164
+ .wpx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .65rem; background: var(--dm-surface); }
165
+ .wpx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
166
+ .wpx .form-label { display: inline-flex; align-items: center; gap: .3rem; margin: 0; font-weight: 600; }
167
+ .wpx-drop { display: flex; align-items: center; justify-content: center; gap: .6rem; flex-wrap: wrap; text-align: center; min-height: 6rem; padding: 1rem;
168
+ border: 2px dashed var(--dm-border); border-radius: var(--dm-radius-md); cursor: pointer; color: var(--dm-text-muted); }
169
+ .wpx-drop:hover, .wpx-drop.is-over, .wpx-drop:focus-within { border-color: var(--dm-primary); color: var(--dm-text); background: color-mix(in srgb, var(--dm-primary) 6%, transparent); }
170
+ .wpx-drop input { position: absolute; width: 1px; height: 1px; opacity: 0; }
171
+ .wpx-bar { height: .5rem; border-radius: 999px; background: color-mix(in srgb, var(--dm-text) 10%, transparent); overflow: hidden; }
172
+ .wpx-bar-fill { display: block; height: 100%; width: 0; background: var(--dm-primary); transition: width .3s; }
173
+ .wpx-upload { display: flex; flex-direction: column; gap: .35rem; }
174
+ .wpx-head { flex-direction: row; align-items: center; flex-wrap: wrap; }
175
+ .wpx-head-main { flex: 1 1 16rem; display: flex; flex-direction: column; min-width: 0; }
176
+ .wpx-site { font-size: var(--dm-text-lg); overflow-wrap: anywhere; }
177
+ .wpx-head-actions { display: flex; gap: .4rem; flex-wrap: wrap; }
178
+ .wpx-options { display: flex; flex-direction: column; gap: .9rem; }
179
+ .wpx-option { display: flex; flex-direction: column; gap: .4rem; }
180
+ .wpx-toggles { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem 1rem; }
181
+ .wpx-toggle { display: flex; align-items: center; gap: .5rem; font-size: var(--dm-text-sm); }
182
+ .wpx-zip { display: flex; flex-direction: column; gap: .4rem; padding: .5rem .65rem; border: 1px dashed var(--dm-border); border-radius: var(--dm-radius-md); }
183
+ .wpx-zip-actions { display: flex; gap: .4rem; align-items: center; }
184
+ .wpx-zip-pick { cursor: pointer; }
185
+ .wpx-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; align-self: flex-start; }
186
+ .wpx-seg button { display: inline-flex; align-items: center; gap: .35rem; border: 0; background: none; color: var(--dm-text); padding: .35rem .75rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
187
+ .wpx-seg button + button { border-left: 1px solid var(--dm-border); }
188
+ .wpx-seg button:hover:not(:disabled) { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
189
+ .wpx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
190
+ .wpx-seg button:disabled { opacity: .45; cursor: not-allowed; }
191
+ .wpx-seg b { font-weight: 600; opacity: .8; }
192
+ .wpx-bar-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
193
+ .wpx-search { max-width: 18rem; }
194
+ .wpx-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); gap: .6rem; }
195
+ .wpx-tile { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .6rem .75rem; background: var(--dm-surface); display: flex; flex-direction: column; }
196
+ .wpx-tile strong { font-size: var(--dm-text-xl, 1.4rem); font-variant-numeric: tabular-nums; }
197
+ .wpx-tile small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
198
+ .wpx-notes { display: flex; flex-direction: column; gap: .35rem; }
199
+ .wpx-note-list { display: flex; flex-direction: column; gap: .35rem; }
200
+ .wpx-list { display: flex; flex-direction: column; }
201
+ .wpx-row { display: flex; align-items: flex-start; gap: .65rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
202
+ .wpx-row:last-child { border-bottom: 0; }
203
+ .wpx-row:hover, .wpx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
204
+ .wpx-row-icon { color: var(--dm-text-muted); padding-top: .15rem; display: inline-flex; }
205
+ .wpx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
206
+ .wpx-main strong { overflow-wrap: anywhere; }
207
+ .wpx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); overflow-wrap: anywhere; }
208
+ .wpx-where { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); color: var(--dm-text); }
209
+ .wpx-reason { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
210
+ .wpx-rebuild { color: var(--dm-warning); font-size: var(--dm-text-xs); font-family: var(--dm-font-mono, monospace); overflow-wrap: anywhere; }
211
+ .wpx-chips { display: flex; gap: .35rem; flex-wrap: wrap; justify-content: flex-end; }
212
+ .wpx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .15rem .55rem; border-radius: 999px; white-space: nowrap;
213
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
214
+ .wpx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
215
+ .wpx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 16%, transparent); color: var(--dm-info, var(--dm-primary)); }
216
+ .wpx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
217
+ .wpx-chip.is-bad { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
218
+ .wpx-chip.is-chosen { box-shadow: inset 0 0 0 1px var(--dm-primary); }
219
+ .wpx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
220
+ .wpx-kebab:hover { color: var(--dm-primary); }
221
+ .wpx-report { display: flex; flex-direction: column; gap: .75rem; padding: .25rem 0; }
222
+ @media (max-width: 720px) { .wpx-toggles { grid-template-columns: 1fr; } .wpx-row { flex-wrap: wrap; } .wpx-chips { justify-content: flex-start; } }
223
+ </style>
@@ -1 +1 @@
1
- import{apiRequest as p}from"../api.js";import{helpPopovers as V}from"../lib/help-popover.js";import{viewLifetime as Y}from"../lib/tool-kit.js";const z=/^#\/calls\/new\/?(\?.*)?$/,G=/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/,f=Y(z);export function openHostTab(){let t=null;try{t=window.open("","_blank")}catch{t=null}if(t)try{t.document.title="Starting the call\u2026",t.document.body.style.font="16px system-ui, sans-serif",t.document.body.textContent="Starting the call\u2026"}catch{}return t}export function aimTab(t,o){if(!t||t.closed)return!1;try{t.opener=null}catch{}return t.location.href=o,!0}const R=t=>(t?.message||String(t||"Something went wrong")).replace(/^HTTP \d+$/,"Something went wrong");export const callNewView={templateUrl:"/admin/js/templates/call-new.html",async onMount(t){f.reset();const o=t.get(0).querySelector(".cnx")||t.get(0),y=new URLSearchParams(location.hash.split("?")[1]||""),g=M.observable(!0),q=M.observable(""),s=M.observable(String(y.get("name")||"").slice(0,120)),i=M.observable(String(y.get("email")||"").slice(0,200)),c=M.observable(""),u=M.observable(""),r=M.observable(!0),m=M.observable(!1),w=M.observable(""),d=M.observable([]),k=M.observable(!1),h=M.observable(!1),v=M.observable(!1),b=M.observable(""),I=M.computed(()=>G.test(String(i.value||"").trim())),S=M.observable(!1),L=M.observable(!1),j=M.observable(""),T=M.observable(""),O=M.observable(""),P=M.observable("");let N=0,C=0;const x=M.effect(()=>{const e=String(w.value||"").trim();clearTimeout(C);const U=++N;if(e.length<2){d.value=[],k.value=!1;return}C=setTimeout(async()=>{try{const l=await p(`/calls/contacts?q=${encodeURIComponent(e)}`,{method:"GET"});if(U!==N)return;const a=(Array.isArray(l)?l:[]).slice(0,8).map(n=>({id:String(n.id),name:n.name||n.email||"(no name)",email:n.email||"",note:n.mine===!1?"shared with you":""}));d.value=a,k.value=a.length===0}catch(l){(l?.status===403||l?.status===404)&&(h.value=!1),d.value=[]}},200)});f.add(()=>{clearTimeout(C);try{typeof x=="function"?x():x?.dispose?.()}catch{}});const A={loading:g,unavailable:q,name:s,email:i,contactId:c,contactName:u,mailReady:r,notify:m,q:w,matches:d,noMatch:k,contactsOn:h,busy:v,err:b,hasEmail:I,started:S,blocked:L,hostUrl:j,joinUrl:T,emailLine:O,mailto:P,pick(e){c.value=e.id,u.value=e.name,s.value=e.name==="(no name)"?"":e.name,i.value=e.email,m.value=!!e.email&&r.peek(),w.value=""},clearContact(){c.value="",u.value="",s.value="",i.value="",m.value=!1},close:()=>{history.length>1?history.back():location.hash="#/"},async copyLink(){try{await navigator.clipboard.writeText(T.peek()),E.toast("Their link is copied",{type:"success",duration:1500})}catch{o.querySelector(".cnx-link")?.select?.(),E.toast("Copy it from the box (Ctrl+C)",{type:"info"})}},selectLink:e=>{try{e?.target?.select?.()}catch{}},save(){if(v.peek()||S.peek())return;const e={name:String(s.peek()||"").trim(),email:String(i.peek()||"").trim()};if(e.email&&!G.test(e.email)){b.value="That email address does not look right.";return}const U=m.peek()===!0&&!!e.email&&r.peek();b.value="";const l=openHostTab();v.value=!0,p("/calls",{method:"POST",body:JSON.stringify({...e,contactId:c.peek(),notify:U})}).then(a=>{const n=new URL(a.hostUrl,location.origin).href,_=aimTab(l,n);j.value=n,T.value=a.joinUrl,L.value=!_;const $=e.name||e.email||"them";O.value=a.emailed===!0?`Emailed ${$} their link.`:a.emailLimited===!0?`Not emailed: ${$} has had several call emails in the last hour. Send them the link below, or read it out.`:e.email?`${$} has not been emailed - send them the link below, or read it out.`:"Send them the link below, or read it out.",P.value=`mailto:${encodeURIComponent(e.email)}?subject=${encodeURIComponent("Join the call")}&body=${encodeURIComponent(`Join the call: ${a.joinUrl}`)}`,S.value=!0,setTimeout(()=>o.querySelector(".cnx-link")?.focus(),60)}).catch(a=>{try{l?.close()}catch{}b.value=a?.status===404?"Calls are not available on this site.":a?.status===429?"That is a lot of calls in a minute - wait a moment and try again.":`No call: ${R(a)}`}).finally(()=>{v.value=!1})}};f.add(V(o)),window.I?.scan?.(o);const J=Object.fromEntries(Object.entries(A).filter(([,e])=>typeof e=="function"&&!M.isObservable?.(e))),H=M.applyBindings(A,o,{methods:J});f.add(()=>{try{H?.dispose?.()}catch{}});try{const e=await p("/calls/status",{method:"GET"});r.value=e?.mailReady!==!1,h.value=e?.contacts===!0}catch(e){g.value=!1,q.value=e?.status===404?"Calls need a Pro Tool with calls - Booking Pro, Contacts Pro or Calendar.":e?.status===403?"Your role cannot start calls. Ask an administrator for the Calls permission.":`Calls could not load: ${R(e)}`;return}const B=y.get("contact");if(B)try{const e=await p(`/calls/contact/${encodeURIComponent(B)}`,{method:"GET"});c.value=e.id,u.value=e.name||e.email||"(no name)",s.value=e.name||"",i.value=e.email||""}catch(e){E.toast(`That contact could not be read: ${R(e)}`,{type:"warning"})}m.value=I.peek()&&r.peek(),g.value=!1,setTimeout(()=>o.querySelector(c.peek()||s.peek()?"#cnx-email":h.peek()?"#cnx-q":"#cnx-name")?.focus(),60)}};
1
+ import{apiRequest as p}from"../api.js";import{helpPopovers as V}from"../lib/help-popover.js";import{viewLifetime as Y}from"../lib/tool-kit.js";import{mailtoHref as z,openEmail as F}from"../lib/mail-compose.js";const K=/^#\/calls\/new\/?(\?.*)?$/,H=/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/,y=Y(K);export function openHostTab(){let t=null;try{t=window.open("","_blank")}catch{t=null}if(t)try{t.document.title="Starting the call\u2026",t.document.body.style.font="16px system-ui, sans-serif",t.document.body.textContent="Starting the call\u2026"}catch{}return t}export function aimTab(t,l){if(!t||t.closed)return!1;try{t.opener=null}catch{}return t.location.href=l,!0}const R=t=>(t?.message||String(t||"Something went wrong")).replace(/^HTTP \d+$/,"Something went wrong");export const callNewView={templateUrl:"/admin/js/templates/call-new.html",async onMount(t){y.reset();const l=t.get(0).querySelector(".cnx")||t.get(0),g=new URLSearchParams(location.hash.split("?")[1]||""),w=M.observable(!0),L=M.observable(""),s=M.observable(String(g.get("name")||"").slice(0,120)),i=M.observable(String(g.get("email")||"").slice(0,200)),r=M.observable(""),u=M.observable(""),c=M.observable(!0),m=M.observable(!1),k=M.observable(""),d=M.observable([]),S=M.observable(!1),h=M.observable(!1),v=M.observable(!1),b=M.observable(""),j=M.computed(()=>H.test(String(i.value||"").trim())),T=M.observable(!1),I=M.observable(!1),O=M.observable(""),x=M.observable(""),P=M.observable(""),N=M.observable("");let f=null,A=0,C=0;const q=M.effect(()=>{const e=String(k.value||"").trim();clearTimeout(C);const U=++A;if(e.length<2){d.value=[],S.value=!1;return}C=setTimeout(async()=>{try{const o=await p(`/calls/contacts?q=${encodeURIComponent(e)}`,{method:"GET"});if(U!==A)return;const a=(Array.isArray(o)?o:[]).slice(0,8).map(n=>({id:String(n.id),name:n.name||n.email||"(no name)",email:n.email||"",note:n.mine===!1?"shared with you":""}));d.value=a,S.value=a.length===0}catch(o){(o?.status===403||o?.status===404)&&(h.value=!1),d.value=[]}},200)});y.add(()=>{clearTimeout(C);try{typeof q=="function"?q():q?.dispose?.()}catch{}});const B={loading:w,unavailable:L,name:s,email:i,contactId:r,contactName:u,mailReady:c,notify:m,q:k,matches:d,noMatch:S,contactsOn:h,busy:v,err:b,hasEmail:j,started:T,blocked:I,hostUrl:O,joinUrl:x,emailLine:P,mailto:N,pick(e){r.value=e.id,u.value=e.name,s.value=e.name==="(no name)"?"":e.name,i.value=e.email,m.value=!!e.email&&c.peek(),k.value=""},clearContact(){r.value="",u.value="",s.value="",i.value="",m.value=!1},close:()=>{history.length>1?history.back():location.hash="#/"},async copyLink(){try{await navigator.clipboard.writeText(x.peek()),E.toast("Their link is copied",{type:"success",duration:1500})}catch{l.querySelector(".cnx-link")?.select?.(),E.toast("Copy it from the box (Ctrl+C)",{type:"info"})}},emailIt(e){f?.to.length&&(e?.preventDefault?.(),F(f))},selectLink:e=>{try{e?.target?.select?.()}catch{}},save(){if(v.peek()||T.peek())return;const e={name:String(s.peek()||"").trim(),email:String(i.peek()||"").trim()};if(e.email&&!H.test(e.email)){b.value="That email address does not look right.";return}const U=m.peek()===!0&&!!e.email&&c.peek();b.value="";const o=openHostTab();v.value=!0,p("/calls",{method:"POST",body:JSON.stringify({...e,contactId:r.peek(),notify:U})}).then(a=>{const n=new URL(a.hostUrl,location.origin).href,D=aimTab(o,n);O.value=n,x.value=a.joinUrl,I.value=!D;const $=e.name||e.email||"them";P.value=a.emailed===!0?`Emailed ${$} their link.`:a.emailLimited===!0?`Not emailed: ${$} has had several call emails in the last hour. Send them the link below, or read it out.`:e.email?`${$} has not been emailed - send them the link below, or read it out.`:"Send them the link below, or read it out.",f={to:e.email?[{name:e.name,address:e.email}]:[],subject:"Join the call",body:`Join the call: ${a.joinUrl}`},N.value=z(f),T.value=!0,setTimeout(()=>l.querySelector(".cnx-link")?.focus(),60)}).catch(a=>{try{o?.close()}catch{}b.value=a?.status===404?"Calls are not available on this site.":a?.status===429?"That is a lot of calls in a minute - wait a moment and try again.":`No call: ${R(a)}`}).finally(()=>{v.value=!1})}};y.add(V(l)),window.I?.scan?.(l);const J=Object.fromEntries(Object.entries(B).filter(([,e])=>typeof e=="function"&&!M.isObservable?.(e))),_=M.applyBindings(B,l,{methods:J});y.add(()=>{try{_?.dispose?.()}catch{}});try{const e=await p("/calls/status",{method:"GET"});c.value=e?.mailReady!==!1,h.value=e?.contacts===!0}catch(e){w.value=!1,L.value=e?.status===404?"Calls need a Pro Tool with calls - Booking Pro, Contacts Pro or Calendar.":e?.status===403?"Your role cannot start calls. Ask an administrator for the Calls permission.":`Calls could not load: ${R(e)}`;return}const G=g.get("contact");if(G)try{const e=await p(`/calls/contact/${encodeURIComponent(G)}`,{method:"GET"});r.value=e.id,u.value=e.name||e.email||"(no name)",s.value=e.name||"",i.value=e.email||""}catch(e){E.toast(`That contact could not be read: ${R(e)}`,{type:"warning"})}m.value=j.peek()&&c.peek(),w.value=!1,setTimeout(()=>l.querySelector(r.peek()||s.peek()?"#cnx-email":h.peek()?"#cnx-q":"#cnx-name")?.focus(),60)}};